package org.bean.spiral.processor;


import java.beans.PropertyDescriptor;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.collections.map.MultiValueMap;
import org.apache.commons.lang.StringUtils;
import org.bean.spiral.annotations.OneToMany;
import org.bean.spiral.annotations.Root;


/**
 * 
 * @author zsaiye
 * 
 */
@SuppressWarnings( { "unchecked" })
public class Resolver {

  /**
   * 
   * @param <E>
   * @param <R>
   * @param root
   * @param element
   * @throws Exception
   */
  public <E, R> void process(R root, E element) throws Exception {

    List<Field> rootFields = getChildren(root.getClass(), getOneToMany(element
        .getClass()), element.getClass().getAnnotation(Root.class).rootClass());
    // Group First Children
    MultiValueMap map = new MultiValueMap();
    for (Field field : rootFields) {
      OneToMany oneToMany = field.getAnnotation(OneToMany.class);
      // Currently for Root only
      map.put(oneToMany.mappedClass(), field);
    }

    Set<Class<?>> keys = map.keySet();
    for (Class<?> clazz : keys) {
      process(
              createInstance(clazz, (List<Field>) map.get(clazz), element, root),
              element);
    }
  }

  /**
   * 
   * @param <T>
   * @param <R>
   * @param root
   * @param instance
   * @param annotation
   * @return
   * @throws Exception
   */
  private <T, R> T validateInstance(R root, T instance, OneToMany annotation)
      throws Exception {

    PropertyDescriptor descriptor = PropertyUtils
        .getPropertyDescriptor(root, annotation.parentCollection());
    List parentCollection = (List) descriptor.getReadMethod()
        .invoke(root, null);
    if (parentCollection.contains(instance)) {
      instance = (T) parentCollection.get(parentCollection.indexOf(instance));
    } else {
      parentCollection.add(instance);
    }
    return instance;
  }

  /**
   * 
   * @param <T>
   * @param <E>
   * @param <R>
   * @param clazz
   * @param fields
   * @param element
   * @param root
   * @return
   * @throws Exception
   */
  private <T, E, R> T createInstance(
    Class<?> clazz,
    List<Field> fields,
    E element,
    R root) throws Exception {

    T newClass = null;

    Constructor ctor = clazz.getConstructor(root.getClass());
    newClass = (T) ctor.newInstance(root);
    OneToMany rootField = null;

    PropertyDescriptor descriptor = null;
    for (Field field : fields) {
      rootField = field.getAnnotation(OneToMany.class);
      String mappedBy = rootField.mappedBy();
      if (StringUtils.equals(mappedBy, OneToMany.AS_FIELD_NAME)) {
        mappedBy = field.getName();
      }
      descriptor = PropertyUtils.getPropertyDescriptor(newClass, mappedBy);
      descriptor.getWriteMethod().invoke(newClass,
                                         getFieldValue(field, element));
    }

    return validateInstance(root, newClass, rootField);

  }

  /**
   * 
   * @param rootClass
   * @param fields
   * @return
   */
  private List<Field> getChildren(
    Class<?> rootClass,
    List<Field> fields,
    Class<?> elementRoot) {

    // Search all OneToMany where parent is rootClass
    List<Field> list = new ArrayList<Field>();
    for (Field field : fields) {
      Class<?> parentClass = field.getAnnotation(OneToMany.class).parent();
      if (parentClass.equals(rootClass)
          || (parentClass.equals(Root.class) && rootClass.equals(elementRoot))) {
        list.add(field);
      }
    }
    return list;
  }

  /**
   * 
   * @param elementClass
   * @return
   */
  private List<Field> getOneToMany(Class<?> elementClass) {

    List<Field> keys = new ArrayList<Field>();
    for (Field field : elementClass.getDeclaredFields()) {
      if (field.getAnnotation(OneToMany.class) != null) {
        keys.add(field);
      }
    }
    return keys;
  }

  /**
   * 
   * @param <E>
   * @param field
   * @param instance
   * @return
   */
  private <E> Object getFieldValue(final Field field, final E instance) {

    return AccessController.doPrivileged(new PrivilegedAction() {

      public Object run() {

        try {
          field.setAccessible(true);
          return field.get(instance);
        } catch (IllegalAccessException e) {
          e.printStackTrace();
        }
        return null;
      }
    });
  }

}
